let TweenEx = {
    tweens: {},
    tweensAddedDuringUpdate: {},
    nextId: 0,
    getAll: function () {
        return Object.keys(this.tweens).map(function (tweenId) {
            return this.tweens[tweenId];
        });
    },
    removeAll: function () {
        this.tweens = {};
    },
    add: function (tween) {
        this.tweens[tween.getId()] = tween;
        this.tweensAddedDuringUpdate[tween.getId()] = tween;
    },
    remove: function (tween) {
        delete this.tweens[tween.getId()];
        delete this.tweensAddedDuringUpdate[tween.getId()];
    },
    update: function (time: number, preserve: boolean = true) {
        let tweenIds = Object.keys(this.tweens);
        if (tweenIds.length === 0) {
            return false;
        }
        while (tweenIds.length > 0) {
            this.tweensAddedDuringUpdate = {};
            for (let i = 0; i < tweenIds.length; i++) {
                if (this.tweens[tweenIds[i]] && this.tweens[tweenIds[i]].update(this.now()) === false && !preserve) {
                    delete this.tweens[tweenIds[i]];
                }
            }
            tweenIds = Object.keys(this.tweensAddedDuringUpdate);
        }
        return true;
    },
    getNextId: function () {
        return this.nextId++;
    },
    now: function () {
        return new Date().getTime();
    }
}
class Tween {
    protected object;
    protected valuesStart;
    protected valuesEnd;
    protected valuesStartRepeat;
    protected duration;
    protected repeat;
    protected repeatDelayTime;
    protected yoyo;
    protected isPlaying;
    protected reversed;
    protected delayTime;
    protected startTime;
    protected easingFunction;
    protected interpolationFunction;
    protected chainedTweens;
    protected onStartCallback;
    protected onStartCallbackFired;
    protected onUpdateCallback;
    protected onCompleteCallback;
    protected onStopCallback;
    protected id;
    constructor(object) {
        this.object = object;
        this.valuesStart = {};
        this.valuesEnd = {};
        this.valuesStartRepeat = {};
        this.duration = 1000;
        this.repeat = 0;
        this.repeatDelayTime = undefined;
        this.yoyo = false;
        this.isPlaying = false;
        this.reversed = false;
        this.delayTime = 0;
        this.startTime = null;
        this.easingFunction = Easing.Linear.None;
        this.interpolationFunction = Interpolation.Linear;
        this.chainedTweens = [];
        this.onStartCallback = null;
        this.onStartCallbackFired = false;
        this.onUpdateCallback = null;
        this.onCompleteCallback = null;
        this.onStopCallback = null;
        this.id = TweenEx.getNextId();
    }
    protected getId() {
        return this.id;
    }
    public to(properties, duration) {
        this.valuesEnd = properties;
        if (duration !== undefined) {
            this.duration = duration;
        }
        return this;
    }
    public start() {
        TweenEx.add(this);
        this.isPlaying = true;
        this.onStartCallbackFired = false;
        this.startTime = TweenEx.now();
        this.startTime += this.delayTime;
        for (let property in this.valuesEnd) {
            if (Array.isArray(this.valuesEnd[property])) {
                if (this.valuesEnd[property].length === 0) {
                    continue;
                }
                this.valuesEnd[property] = [this.object[property]].concat(this.valuesEnd[property]);
            }
            if (this.object[property] === undefined) {
                continue;
            }
            this.valuesStart[property] = this.object[property];
            if (!Array.isArray(this.valuesStart[property])) {
                this.valuesStart[property] *= 1.0;
            }
            this.valuesStartRepeat[property] = this.valuesStart[property] || 0;
        }
        return this;
    }
    public stop() {
        if (!this.isPlaying) {
            return this;
        }
        TweenEx.remove(this);
        this.isPlaying = false;
        if (this.onStopCallback !== null) {
            this.onStopCallback.call(this.object, this.object);
        }
        this.stopChainedTweens();
        return this;
    }
    public end() {
        this.update(this.startTime + this.duration);
        return this;
    }
    public stopChainedTweens() {
        for (let i = 0, numChainedTweens = this.chainedTweens.length; i < numChainedTweens; i++) {
            this.chainedTweens[i].stop();
        }
    }
    public delay(amount) {
        this.delayTime = amount;
        return this;
    }
    public setRepeat(times) {
        this.repeat = times;
        return this;
    }
    public repeatDelay(amount) {
        this.repeatDelayTime = amount;
        return this;
    }
    public setYoyo(yoyo) {
        this.yoyo = yoyo;
        return this;
    }
    public easing(easing) {
        this.easingFunction = easing;
        return this;
    }
    public interpolation(interpolation) {
        this.interpolationFunction = interpolation;
        return this;
    }
    public chain() {
        this.chainedTweens = arguments;
        return this;
    }
    public onStart(callback) {
        this.onStartCallback = callback;
        return this;
    }
    public onUpdate(callback) {
        this.onUpdateCallback = callback;
        return this;
    }
    public onComplete(callback) {
        this.onCompleteCallback = callback;
        return this;
    }
    public onStop(callback) {
        this.onStopCallback = callback;
        return this;
    }
    protected update(time) {
        let property;
        let elapsed;
        let value;
        if (time < this.startTime) {
            return true;
        }
        if (this.onStartCallbackFired === false) {
            if (this.onStartCallback !== null) {
                this.onStartCallback.call(this.object, this.object);
            }
            this.onStartCallbackFired = true;
        }
        elapsed = (time - this.startTime) / this.duration;
        elapsed = elapsed > 1 ? 1 : elapsed;
        value = this.easingFunction(elapsed);
        for (property in this.valuesEnd) {
            if (this.valuesStart[property] === undefined) {
                continue;
            }
            let start = this.valuesStart[property] || 0;
            let end = this.valuesEnd[property];
            if (Array.isArray(end)) {
                this.object[property] = this.interpolationFunction(end, value);
            } else {
                if (typeof (end) === 'string') {
                    if (end.charAt(0) === '+' || end.charAt(0) === '-') {
                        end = start + parseFloat(end);
                    } else {
                        end = parseFloat(end);
                    }
                }
                if (typeof (end) === 'number') {
                    this.object[property] = start + (end - start) * value;
                }
            }
        }
        if (this.onUpdateCallback !== null) {
            this.onUpdateCallback.call(this.object, value);
        }
        if (elapsed === 1) {
            if (this.repeat > 0) {
                if (isFinite(this.repeat)) {
                    this.repeat--;
                }
                for (property in this.valuesStartRepeat) {
                    if (typeof (this.valuesEnd[property]) === 'string') {
                        this.valuesStartRepeat[property] = this.valuesStartRepeat[property] + parseFloat(this.valuesEnd[property]);
                    }
                    if (this.yoyo) {
                        let tmp = this.valuesStartRepeat[property];
                        this.valuesStartRepeat[property] = this.valuesEnd[property];
                        this.valuesEnd[property] = tmp;
                    }
                    this.valuesStart[property] = this.valuesStartRepeat[property];
                }
                if (this.yoyo) {
                    this.reversed = !this.reversed;
                }
                if (this.repeatDelayTime !== undefined) {
                    this.startTime = time + this.repeatDelayTime;
                } else {
                    this.startTime = time + this.delayTime;
                }
                return true;
            } else {
                if (this.onCompleteCallback !== null) {
                    this.onCompleteCallback.call(this.object, this.object);
                }
                for (let i = 0, numChainedTweens = this.chainedTweens.length; i < numChainedTweens; i++) {
                    this.chainedTweens[i].start(this.startTime + this.duration);
                }
                return false;
            }
        }
        return true;
    }
}
let
    Easing = {
        Linear: {
            None: function (k) {
                return k;
            }
        },
        Quadratic: {
            In: function (k) {
                return k * k;
            },
            Out: function (k) {
                return k * (2 - k);
            },
            InOut: function (k) {
                if ((k *= 2) < 1) {
                    return 0.5 * k * k;
                }
                return -0.5 * (--k * (k - 2) - 1);
            }
        },
        Cubic: {
            In: function (k) {
                return k * k * k;
            },
            Out: function (k) {
                return --k * k * k + 1;
            },
            InOut: function (k) {
                if ((k *= 2) < 1) {
                    return 0.5 * k * k * k;
                }
                return 0.5 * ((k -= 2) * k * k + 2);
            }
        },

        Quartic: {
            In: function (k) {
                return k * k * k * k;
            },
            Out: function (k) {
                return 1 - (--k * k * k * k);
            },
            InOut: function (k) {
                if ((k *= 2) < 1) {
                    return 0.5 * k * k * k * k;
                }
                return -0.5 * ((k -= 2) * k * k * k - 2);
            }
        },
        Quintic: {
            In: function (k) {
                return k * k * k * k * k;
            },
            Out: function (k) {
                return --k * k * k * k * k + 1;
            },
            InOut: function (k) {
                if ((k *= 2) < 1) {
                    return 0.5 * k * k * k * k * k;
                }
                return 0.5 * ((k -= 2) * k * k * k * k + 2);
            }
        },
        Sinusoidal: {
            In: function (k) {
                return 1 - Math.cos(k * Math.PI / 2);
            },
            Out: function (k) {
                return Math.sin(k * Math.PI / 2);
            },
            InOut: function (k) {
                return 0.5 * (1 - Math.cos(Math.PI * k));
            }
        },
        Exponential: {
            In: function (k) {
                return k === 0 ? 0 : Math.pow(1024, k - 1);
            },
            Out: function (k) {
                return k === 1 ? 1 : 1 - Math.pow(2, -10 * k);
            },
            InOut: function (k) {
                if (k === 0) {
                    return 0;
                }
                if (k === 1) {
                    return 1;
                }
                if ((k *= 2) < 1) {
                    return 0.5 * Math.pow(1024, k - 1);
                }
                return 0.5 * (-Math.pow(2, -10 * (k - 1)) + 2);
            }
        },
        Circular: {
            In: function (k) {
                return 1 - Math.sqrt(1 - k * k);
            },
            Out: function (k) {
                return Math.sqrt(1 - (--k * k));
            },
            InOut: function (k) {
                if ((k *= 2) < 1) {
                    return -0.5 * (Math.sqrt(1 - k * k) - 1);
                }
                return 0.5 * (Math.sqrt(1 - (k -= 2) * k) + 1);
            }
        },
        Elastic: {
            In: function (k) {
                if (k === 0) {
                    return 0;
                }
                if (k === 1) {
                    return 1;
                }
                return -Math.pow(2, 10 * (k - 1)) * Math.sin((k - 1.1) * 5 * Math.PI);
            },
            Out: function (k) {
                if (k === 0) {
                    return 0;
                }
                if (k === 1) {
                    return 1;
                }
                return Math.pow(2, -10 * k) * Math.sin((k - 0.1) * 5 * Math.PI) + 1;
            },
            InOut: function (k) {
                if (k === 0) {
                    return 0;
                }
                if (k === 1) {
                    return 1;
                }
                k *= 2;
                if (k < 1) {
                    return -0.5 * Math.pow(2, 10 * (k - 1)) * Math.sin((k - 1.1) * 5 * Math.PI);
                }
                return 0.5 * Math.pow(2, -10 * (k - 1)) * Math.sin((k - 1.1) * 5 * Math.PI) + 1;
            }
        },
        Back: {
            In: function (k) {
                let s = 1.70158;
                return k * k * ((s + 1) * k - s);
            },
            Out: function (k) {
                let s = 1.70158;
                return --k * k * ((s + 1) * k + s) + 1;
            },
            InOut: function (k) {
                let s = 1.70158 * 1.525;
                if ((k *= 2) < 1) {
                    return 0.5 * (k * k * ((s + 1) * k - s));
                }
                return 0.5 * ((k -= 2) * k * ((s + 1) * k + s) + 2);
            }
        },
        Bounce: {
            In: function (k) {
                return 1 - this.Easing.Bounce.Out(1 - k);
            },
            Out: function (k) {
                if (k < (1 / 2.75)) {
                    return 7.5625 * k * k;
                } else if (k < (2 / 2.75)) {
                    return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75;
                } else if (k < (2.5 / 2.75)) {
                    return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375;
                } else {
                    return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375;
                }
            },
            InOut: function (k) {
                if (k < 0.5) {
                    return this.Easing.Bounce.In(k * 2) * 0.5;
                }
                return this.Easing.Bounce.Out(k * 2 - 1) * 0.5 + 0.5;
            }
        }
    };

let Interpolation = {
    Linear: function (v, k) {
        let m = v.length - 1;
        let f = m * k;
        let i = Math.floor(f);
        let fn = this.Interpolation.Utils.Linear;
        if (k < 0) {
            return fn(v[0], v[1], f);
        }
        if (k > 1) {
            return fn(v[m], v[m - 1], m - f);
        }
        return fn(v[i], v[i + 1 > m ? m : i + 1], f - i);
    },
    Bezier: function (v, k) {
        let b = 0;
        let n = v.length - 1;
        let pw = Math.pow;
        let bn = this.Interpolation.Utils.Bernstein;
        for (let i = 0; i <= n; i++) {
            b += pw(1 - k, n - i) * pw(k, i) * v[i] * bn(n, i);
        }
        return b;
    },
    CatmullRom: function (v, k) {
        let m = v.length - 1;
        let f = m * k;
        let i = Math.floor(f);
        let fn = this.Interpolation.Utils.CatmullRom;
        if (v[0] === v[m]) {
            if (k < 0) {
                i = Math.floor(f = m * (1 + k));
            }
            return fn(v[(i - 1 + m) % m], v[i], v[(i + 1) % m], v[(i + 2) % m], f - i);
        } else {
            if (k < 0) {
                return v[0] - (fn(v[0], v[0], v[1], v[1], -f) - v[0]);
            }
            if (k > 1) {
                return v[m] - (fn(v[m], v[m], v[m - 1], v[m - 1], f - m) - v[m]);
            }
            return fn(v[i ? i - 1 : 0], v[i], v[m < i + 1 ? m : i + 1], v[m < i + 2 ? m : i + 2], f - i);
        }
    },
    Utils: {
        Linear: function (p0, p1, t) {
            return (p1 - p0) * t + p0;
        },
        Bernstein: function (n, i) {
            let fc = this.Interpolation.Utils.Factorial;
            return fc(n) / fc(i) / fc(n - i);
        },
        Factorial: (function () {
            let a = [1];
            return function (n) {
                let s = 1;
                if (a[n]) {
                    return a[n];
                }
                for (let i = n; i > 1; i--) {
                    s *= i;
                }
                a[n] = s;
                return s;
            };
        })(),
        CatmullRom: function (p0, p1, p2, p3, t) {
            let v0 = (p2 - p0) * 0.5;
            let v1 = (p3 - p1) * 0.5;
            let t2 = t * t;
            let t3 = t * t2;
            return (2 * p1 - 2 * p2 + v0 + v1) * t3 + (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t2 + v0 * t + p1;
        }
    }
}
export { TweenEx, Tween, Easing, Interpolation };
